﻿namespace DatabaseDoc
{
    using System;
    using System.Collections.Generic;
    using System.Linq;

    using DatabaseDoc.Library;
    using DatabaseDoc.Parameters;

    /// <summary>
    /// The main program
    /// </summary>
    public class Program
    {
        private const string ApplicationName = "databasedoc";

        private static Parameters.Parameters parameters;

        private static ParameterConfiguration parameterConfiguration;

        /// <summary>
        /// The main entry point for the application
        /// </summary>
        /// <param name="arguments">The arguments.</param>
        private static void Main(string[] arguments)
        {          
            parameterConfiguration = GetParameterConfiguration(arguments);

            try
            {
                var paramterValidation = new ParameterValidation(parameterConfiguration);

                if (paramterValidation.IsHelp(arguments))
                {
                    DisplayArguments();
                    return;
                }

                parameters = paramterValidation.GenerateParameters(arguments);
        
                if (parameters == null)
                {
                    throw new InvalidParametersException("Parameters are invalid");
                }
            }
            catch (Exception exception)
            {
                Console.WriteLine("Invalid arguments");
                Console.WriteLine(exception.Message);
                Console.WriteLine();
                DisplayArguments();
                return;
            }     

            // TODO: Add list of parameters for connector and outputter, e.g. /c:MSSQL /cp:foo=bar,foo2=bar2 etc OR /c:key=MSSQL(default),foo=bar,foo2=bar2 etc 
            // TODO: pass parameters onto connector and outputter                      
            // TODO: add support for multiple outputters in one statement
            var serverName = parameters.GetParameterValue<string>("s");
            var databaseName = parameters.GetParameterValue<string>("d");
            var tableName = parameters.GetParameterValue<string>("t");
            var connectorKey = parameters.GetParameterValue("c", "MSSQL");
            var outputterKey = parameters.GetParameterValue("o", "BASICHTML");                        

            var dateTimeAsString = DateTime.Now.ToString("yyyyMMdd-hhmmss");
            var outputterFilePrefix = parameters.GetParameterValue("f", dateTimeAsString);
            var generateScriptPrefix = parameters.GetParameterValue("g", dateTimeAsString);

            var dropDatabase = false;

            try
            {
                // If no database is specified, then create one
                if (string.IsNullOrWhiteSpace(databaseName))
                {
                    Console.WriteLine("No database specified, a temporary database will be created, and then removed once the process is complete");

                    databaseName = GenerateDatabaseName();

                    Console.Write("Database '{0}' will be created on server '{1}', proceed? (Y/N):", databaseName, serverName);
                    var create = Console.ReadLine().ToBoolean();
                    if (create == false)
                    {
                        Console.WriteLine("Process aborted.");
                        return;
                    }
                    
                    var databaseDoc = new DatabaseDoc(connectorKey);
                    databaseDoc.CreateDatabase(serverName, databaseName);
                    dropDatabase = true;
                }

                // Run any delta scripts
                if (parameters.IsParameterSpecified("rv"))
                {
                    Console.WriteLine("Running delta scripts");
                    
                    var runScriptParameter = parameters.GetParameter("rv");
                    var success = RunDeltaScripts(connectorKey, serverName, databaseName, runScriptParameter.Value);                                                         
                    if (!success)
                    {
                        return;
                    }
                }

                // Generate script from database
                if (parameters.IsParameterSpecified("g"))
                {
                    Console.WriteLine("Generating database script");
                    GenerateScript(connectorKey, serverName, databaseName, tableName, generateScriptPrefix);
                }

                // Output database documentation
                if (parameters.IsParameterSpecified("f"))
                {
                    Console.WriteLine("Generating database documentation");
                    CreateDocumentation(connectorKey, serverName, databaseName, tableName, outputterKey, outputterFilePrefix);
                }                
            }
            catch (Exception exception)
            {
                Console.WriteLine("There was an general error:");

                if (parameters.IsParameterSpecified("e"))
                {
                    Console.WriteLine(exception.ToString());
                    return;
                }

                Console.WriteLine(exception.Message);
            }
            finally
            {
                if (dropDatabase)
                {
                    Console.WriteLine("Dropping temporary database '{0}' on server '{1}'", databaseName, serverName);
                    var databaseDoc = new DatabaseDoc(connectorKey);
                    databaseDoc.DropDatabase(serverName, databaseName);
                }
            }
        }

        private static string GenerateDatabaseName()
        {
            var databaseName = string.Format("dd_{0}", Guid.NewGuid().ToString().Replace("-", string.Empty));
            return databaseName;
        }

        private static void GenerateScript(string connectorKey, string serverName, string databaseName, string tableName, string generateScriptPrefix)
        {
            var databaseDoc = new DatabaseDoc(connectorKey);
            databaseDoc.GenerateScripts(serverName, databaseName, tableName, generateScriptPrefix);
        }

        private static bool RunDeltaScripts(string connectorKey, string serverName, string databaseName, string runScriptValue)
        {
            var databaseDoc = new DatabaseDoc(connectorKey);

            if (!string.IsNullOrWhiteSpace(databaseName))
            {
                if (!databaseDoc.DatabaseExists(serverName, databaseName))
                {
                    bool? create = null;
                    while (create == null)
                    {
                        Console.Write("Database '{0}' on server '{1}' does not exist, create it? (Y/N):", databaseName, serverName);
                        create = Console.ReadLine().ToBoolean();
                    }
                    
                    if (!create.Value)
                    {
                        return false;
                    }
                    
                    databaseDoc.CreateDatabase(serverName, databaseName);                    
                }
            }

            var runScriptVersions = databaseDoc.GetRunScriptVersions(runScriptValue);

            if (runScriptVersions.Count() == 0)
            {
                Console.WriteLine("No version scripts found to run, please ensure your scripts are valid version numbers in the format X.X.X.X.sql");
                return false;
            }

            Console.WriteLine("The following *.sql files will be run in this order:");
            foreach (var runScript in runScriptVersions)
            {
                Console.WriteLine(runScript.FileInfo.Name);
            }
            
            bool? run = null;
            while (run == null)
            {
                Console.Write("Proceed? (Y/N):");
                run = Console.ReadLine().ToBoolean();
            }
            
            if (!run.Value)
            {
                Console.WriteLine("Process aborted.");
                return false;
            }

            databaseDoc.RunScripts(serverName, databaseName, runScriptVersions.Select(sv => sv.FileInfo));

            return true;
        }

        private static void RunAllDeltaScripts(string connectorKey, string serverName, string databaseName)
        {
            throw new NotImplementedException();
        }

        private static void CreateDocumentation(string connectorKey, string serverName, string databaseName, string tableName, string outputterKeyName, string outputterFilePrefix)
        {
            // TODO: currently only 1 server supported at a time
            var serverNames = new[] { serverName };

            var databaseDoc = new DatabaseDoc(connectorKey);

            databaseDoc = databaseDoc.Populate(serverNames, databaseName, tableName);
            databaseDoc.Output(outputterFilePrefix, outputterKeyName);
        }

        /// <summary>
        /// Gets the parameter configuration.
        /// </summary>
        /// <param name="arguments">The arguments.</param>
        /// <returns>The parameter configuration</returns>
        private static ParameterConfiguration GetParameterConfiguration(string[] arguments)
        {
            var configuration = new ParameterConfiguration(arguments);

            // TODO: add Description and DefaultValue properties to Parameter
            configuration.AddParameter("s", ParameterRequirement.Required, ParameterOptions.RequiredSingle("servername", "servername", ""));
            configuration.AddParameter("d", ParameterRequirement.Optional, ParameterOptions.RequiredSingle("databasename", "databasename", ""));
            configuration.AddParameter("t", ParameterRequirement.Optional, ParameterOptions.RequiredSingle("tablename", "tablename", ""));
            configuration.AddParameter("c", ParameterRequirement.Optional, ParameterOptions.RequiredSingle("connector", "connector", ""));
            configuration.AddParameter("o", ParameterRequirement.Optional, ParameterOptions.RequiredSingle("outputter", "outputter", ""));
            configuration.AddParameter("f", ParameterRequirement.Optional, ParameterOptions.OptionalSingle("outputterfileprefix", "outputterfileprefix", ""));
            configuration.AddParameter("rv", ParameterRequirement.Optional, ParameterOptions.OptionalSingle("version", "version", ""));
            configuration.AddParameter("g", ParameterRequirement.Optional, ParameterOptions.OptionalSingle("scriptprefix", "scriptprefix", ""));
            configuration.AddParameter("y", ParameterRequirement.Optional, ParameterOptions.None());
            configuration.AddParameter("e", ParameterRequirement.Optional, ParameterOptions.None());

            // If table (t) is specified, then database (d) must also be specified
            configuration.AddParameterDependency("t", "d", DependencyRequirement.Required);

            // If run script (r) is specified, then database (d) must also be specified
            // configuration.AddParameterDependency("r", "d", DependencyRequirement.Required);

            // If generate (g) is specified, then either database (d) or run script (r/rv) must also be specified (or both)
            configuration.AddParameterRule("g", GenerateOptionRule);

            return configuration;
        }

        private static void GenerateOptionRule(Parameter param, IEnumerable<Parameter> otherparameters)
        {
            if (otherparameters == null || otherparameters.Count() == 0)
            {
                return;
            }
                
            if (!(otherparameters.Any(p => p.Matches("r")) || otherparameters.Any(p => p.Matches("rv"))) && !otherparameters.Any(p => p.Matches("d")))
            {
                throw new InvalidParametersException(
                    string.Format(
                        "If you specify the generate option (g), then you must either specify a database (d) or a run script option (r/rv), or both"));
            }
        }

        /// <summary>
        /// Displays the arguments.
        /// </summary>
        private static void DisplayArguments()
        {
            Console.WriteLine("{0} {1}", ApplicationName, parameterConfiguration.GetParameterList());            
            Console.WriteLine();
            Console.WriteLine("Examples");

            foreach (var example in parameterConfiguration.GetExamples())
            {
                Console.WriteLine(example);
            }            
        }
    }

    internal static class StringExtensions
    {
        public static bool? ToBoolean(this string value, bool? defaultValue = null)
        {            
            if (string.IsNullOrWhiteSpace(value))
            {
                return defaultValue;
            }

            if (string.Compare(value, "Y", true) == 0)
            {
                return true;
            }

            if (string.Compare(value, "N", true) == 0)
            {
                return false;
            }

            return defaultValue;
        }
    }
}